package main.tcp;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;


public class NioTcpServer implements Runnable, TcpServer {
	private final int port;
	private ServerSocketChannel ssc;
	private Selector selector;
	private ByteBuffer buf = ByteBuffer.allocate(1024);

	private Class appClass;
	private HashMap<String, TcpApp> apps = new HashMap<String, TcpApp>();

	public NioTcpServer(int port) throws IOException {

		this.port = port;

		this.ssc = ServerSocketChannel.open();

		this.ssc.socket().bind(new InetSocketAddress(port));
		this.ssc.configureBlocking(false);
		this.selector = Selector.open();

		this.ssc.register(selector, SelectionKey.OP_ACCEPT);
	}

	public void setAppClass(Class appClass) {
		this.appClass = appClass;
	}

	@Override
	public void run() {
		try {
			SelectionKey key;
			Iterator<SelectionKey> iter;

			System.out.println("Server starting on port: " + this.port);

			while (this.ssc.isOpen()) {
				selector.select();

				iter = this.selector.selectedKeys().iterator();
				while (iter.hasNext()) {
					key = iter.next();
					iter.remove();

					if (key.isAcceptable()) {
						this.handleAccept(key);
					}
					if (key.isReadable()) {
						this.handleRead(key);
					}
				}

			}
		}
		catch (Exception e) {
			System.out.println("Exception, server on port " + this.port + " terminated.");
			e.printStackTrace();
		}
	}

	private void handleAccept(SelectionKey key) throws IOException, InstantiationException, IllegalAccessException {
		SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();

		String host = sc.socket().getInetAddress().toString();
		String port = Integer.toString(sc.socket().getPort());
		String address = host + ":" + port;

		sc.configureBlocking(false);
		sc.register(selector, SelectionKey.OP_READ, address);

		System.out.println("accepted connection from: " + address);

		key.attach(address);

		TcpApp app = (TcpApp)appClass.newInstance();
		app.setServer(this);
		app.setup();
		apps.put(address, app);
	}

	private void handleRead(SelectionKey key) throws IOException {
		SocketChannel sc = (SocketChannel) key.channel();
		StringBuilder sb = new StringBuilder();

		String host = sc.socket().getInetAddress().toString();
		String port = Integer.toString(sc.socket().getPort());
		String address = host + ":" + port;

		TcpApp app = apps.get(address);

		int read;
		String msg;

		buf.clear();

		while ((read = sc.read(buf)) > 0) {
			buf.flip();
			byte[] bytes = new byte[buf.limit()];
			buf.get(bytes);
			sb.append(new String(bytes));
			buf.clear();
		}

		if (read < 0) {
			// disconnect
			if (app != null) {
				app.onClose(address);
			}
			sc.close();
		}
		else {
			// new msg
			if (app != null) {
				app.onMsg(address, sb.toString());
			}
		}

	}


	@Override
	public void broadcast(String msg) throws IOException {
		msg += "\n";
		ByteBuffer msgBuf = ByteBuffer.wrap(msg.getBytes());
		for (SelectionKey key : selector.keys()) {
			if (key.isValid() && key.channel() instanceof SocketChannel) {
				SocketChannel sch = (SocketChannel) key.channel();
				sch.write(msgBuf);
				msgBuf.rewind();
			}
		}
	}

	@Override
	public void send(String address, String msg) throws IOException {
		msg += "\n";
		ByteBuffer msgBuf = ByteBuffer.wrap(msg.getBytes());
		for (SelectionKey key : selector.keys()) {
			if (key.attachment().toString().equals(address) && key.isValid() && key.channel() instanceof SocketChannel) {
				SocketChannel sch = (SocketChannel) key.channel();
				sch.write(msgBuf);
				msgBuf.rewind();
			}
		}
	}

}